	# gozerbot/irc.py
#
#

""" this module contains the Irc class """

__copyright__ = 'this file is in the public domain'

from gozerbot.generic import rlog, handle_exception, getrandomnick
from gozerbot.wait import Wait
from gozerbot.more import More
from gozerbot.config import config
from gozerbot.eventhandler import Irchandler
from gozerbot.ircevent import Ircevent
from gozerbot.monitor import saymonitor
from gozerbot.less import less
import gozerbot.thr as thr
import time, thread, socket, threading

class Irc(object):

    """ the irc class, provides interface to irc related stuff """

    def __init__(self, name='main'):
        self.name = name # name of irc object
        self.more = More() # more object for cached responses
        self.wait = Wait() # wait object for callbacks on ievent.cmnd
        self.lastoutput = 0 # time of last output .. used for flood protection
        self.outputlock = thread.allocate_lock() # also used for flood protect
        # event thats can be waited on until the irc object is connected to
        # the server
        self.connected = threading.Event()
        self.fsock = None # file socket .. set after connect
        self.sock = None # socket .. set after connect
        self.nick = 'cb1' # bot nick
        self.orignick = self.nick # original wanted nick
        self.server = 'localhost' # server hostname
        self.port = 6667 # server port
        self.password = None # server password
        self.ipv6 = None # ipv6 flag
        self.stopped = 0 # set if bot is stopped
        self.stopreadloop = 0 # set if readloop thread has to be stopped
        # see if there is a nolimiter config option set
        self.nolimiter = config['nolimiter']
        self.reconnectcount = 0 # reconnction counter
        self.eventhandler = Irchandler()
        self.pongcheck = 0
        self.blocking = config['blocking'] or 1

    def connect(self, nick, server, port, password=None, ipv6=None, \
reconnect=True):
        """ connect to server/port using nick """
        try:
            return self.doconnect(nick, server, port, password, ipv6)
        except Exception, ex:
            if self.stopped:
                return
            rlog(10, self.name, str(ex))
            if reconnect:
                self.reconnect()

    def doconnect(self, nick, server, port, password, ipv6):
        """ connect to server/port using nick """
        self.nick = str(nick)
        self.orignick = self.nick
        self.server = str(server)
        self.port = int(port)
        self.password = password
        self.ipv6 = ipv6
        # create socket
        if ipv6:
            rlog(1, self.name, 'creating ipv6 socket')
            self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
            self.ipv6 = 1
        else:
            rlog(0, self.name, 'creating ipv4 socket')
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # optional bind
        elite = config['bindhost']
        if elite:
            try:
                self.sock.bind((elite, 0))
            except socket.gaierror:
                rlog(10, self.name, "can't bind to %s" % elite)
        # do the connect
        rlog(10, self.name, 'connecting to ' + self.server)
        self.sock.settimeout(30)
        self.sock.connect((self.server, int(self.port)))
        # we are connected
        rlog(10, self.name, 'connection ok')
        # make file socket
        self.fsock = self.sock.makefile("r")
        # set socket time out
        self.sock.setblocking(self.blocking)
        self.fsock._sock.setblocking(self.blocking)
        if self.blocking:
            socktimeout = config['socktimeout']
            if not socktimeout:
                socktimeout = 301.0
            else:
                socktimeout = float(socktimeout)
            self.sock.settimeout(socktimeout)
            self.fsock._sock.settimeout(socktimeout)
        # start readloop
        rlog(0, self.name, 'starting readloop')
        thr.start_new_thread(self.readloop, ())
        # if password is provided send it
        if self.password:
            rlog(10, self.name ,'sending password')
            self.send("PASS %s" % password)
        # register with irc server
        rlog(10, self.name, 'registering with server %s using nick: %s' % \
(self.server, self.nick))
        rlog(10, self.name, 'this may take a while')
        self.send("NICK %s" % self.nick)
        username = config['username']
        if not username:
            username = self.nick
        realname = config['realname']
        if not realname:
            realname = username
        self.send("USER %s localhost localhost :%s" % (username, realname))
        # wait on login
        self.connected.wait()
        # registration ok
        rlog(10, self.name, 'logged on !')
        # execute onconnect file
        onconnect = 'onconnect-%s' % self.name
        try:
            execfile(onconnect, globals(), locals())
            rlog(5, self.name, '%s done' % onconnect)
        except Exception, ex:
            try:
                (errnr, error) = ex
                if errnr == 2:
                    pass
                else:
                    rlog(5, self.name, 'error excecuting %s: %s' % \
(onconnect, str(error)))
            except:
                rlog(5, self.name, 'error excecuting %s: %s' % \
(onconnect, str(ex)))
        self.reconnectcount = 0
        self.stopped = 0
        return 1

    def readloop(self):
        """ loop on the socketfile """
        self.stopreadloop = 0
        doreconnect = 0
        timeout = 1
        while not self.stopreadloop:
            try:
                res = self.fsock.readline()
                # if res == "" the other side has disconnected
                if not res and not self.stopreadloop:
                    doreconnect = 1
                    break
                if self.stopreadloop:
                    break 
                res = res.strip()
                rlog(1, self.name, res)
                # parse txt read into an ircevent
                try:
                    ievent = Ircevent().parse(self, res)
                except Exception, ex:
                    handle_exception()
                    continue
                # if succesfull handle the ircevent
                if ievent:
                    self.eventhandler.put(ievent.speed, self, ievent)
                timeout = 1
            except socket.timeout:
                if self.stopreadloop:
                    break
                timeout += 1
                if timeout > 2:
                    doreconnect = 1
                    rlog(10, self.name, 'no pong received')
                    break
                rlog(1, self.name, "socket timeout")
                pingsend = self.ping()
                if not pingsend:
                    doreconnect = 1
                    break
                continue
            except Exception, ex:
                if self.stopreadloop:
                    break
                try:
                    (errno, msg) = ex
                except:
                    errno = -1
                    msg = str(ex)
                # check for temp. unavailable error .. raised when using
                # nonblocking socket
                if errno == 35 or errno == 11:
                    time.sleep(0.5)
                    continue
                rlog(10, self.name, "error in readloop: %s" % msg)
                doreconnect = 1
                break
        rlog(10, self.name, 'readloop stopped')
        # see if we need to reconnect
        if doreconnect:
            self.reconnect()

    def shutdown(self):
        """ shutdown sockets """
        rlog(10, self.name, 'shutdown')
        self.stopreadloop = 1
        self.connected.clear()
        time.sleep(1)
        try:
            self.sock.shutdown(2)
        except:
            pass
        try:
            self.sock.close()
            self.fsock.close()
        except:
            pass

    def reconnect(self):
        """ reconnect to the irc server """
        rlog(10, self.name, 'reconnecting')
        if self.reconnectcount > 0:
            reconsleep = self.reconnectcount*60
            rlog(10, self.name, 'sleeping %s seconds' % reconsleep)
            time.sleep(reconsleep)
        self.reconnectcount += 1
        # determine how many seconds to sleep
        self.shutdown()
        self.connect(self.nick, self.server, self.port, self.password, \
self.ipv6)

    def handle_pong(self, ievent):
        """ set pongcheck on received pong """
        rlog(1, self.name, 'received server pong')
        self.pongcheck = 1

    def sendraw(self, txt):
        """ send raw text to the server """
        rlog(1, self.name + '.sending', txt)
        self.sock.send(txt+'\n')

    def fakein(self, txt):
        """ do a fake ircevent """
        rlog(1, self.name + '.fakein', txt)
        self.handle_ievent(Ircevent().parse(txt))

    def say(self, printto, what, who=None, how='msg', fromm=None):
        """ say what to printto """
        # check if printto is a socket
        if type(printto) == socket.SocketType:
            try:
                printto.send(what + '\n')
            except Exception, ex :
                time.sleep(0.01)
            return
        # if who is set add "who: " to txt
        if who:
            what = "%s: %s" % (who, what)
        self.out(printto, what, who, how, fromm) 

    def out(self, printto, what, who=None, how='msg', fromm=None):
        """ output the first 380 chars .. put the rest into more cache """
        try:
            what = unicode(what)
            what = str(what.encode("ascii", "ignore"))
        except UnicodeError:
            try:
                what = str(what.decode("ascii", "ignore"))
            except UnicodeError:
                handle_exception()
                return
        txtlist = []
        if len(what) <= 375:
            txtlist.append(what)
        else:
            teller = 0
            end = 376
            temptxt = ""
            for char in what:
                temptxt += char
                teller += 1
                if teller / end:
                    if char != ' ' and end < 400:
                        end += 1
                        continue
                    else:
                        txtlist.append(str(temptxt))
                        temptxt = ""
                        end = 375
                        teller = 0
            if temptxt:
                txtlist.append(str(temptxt))
        size = 0
        if len(txtlist) > 1:
            if not fromm:
                self.more.set(printto, txtlist[1:])
                less.add(printto, txtlist)
            else:
                self.more.set(fromm, txtlist[1:])
                less.add(fromm, txtlist)
            size = len(txtlist) - 1
            result = txtlist[:1][0]
            if size:
                result += " (+%s)" % size
        else:
            result = txtlist[0]
        self.output(printto, result, how, who, fromm)

    def output(self, printto, what, how , who=None, fromm=None):
        """ first output .. then call saymonitor """
        self.outputnolog(printto, what, how, who, fromm)
        saymonitor.callcb(self.name, printto, what, who, how, fromm)
        
    def outputnolog(self, printto, what, how, who=None, fromm=None):
        """ do output to irc server .. rate limit to 3 sec """
        self.outputlock.acquire()
        try:
            now = time.time()
            if (now - self.lastoutput) < 3.0:
                if not self.nolimiter:
                    rlog(1, self.name, 'flood protect')
                    time.sleep(3)
            self.lastoutput = time.time()
            if what:
                if how == 'msg':
                    self.privmsg(printto, what)
                elif how == 'notice':
                    self.notice(printto, what)
                elif how == 'ctcp':
                    self.ctcp(printto, what)
        finally:
            self.outputlock.release()

    def setnick(self, nick):
        """ set nick """
        self.orignick = nick
        self.nick = nick
        self.send('NICK %s' % nick)

    def changenick(self, nick):
        """ change nick """
        self.nick = nick
        self.send('NICK %s' % nick)

    def join(self, channel, password=None):
        """ join channel with optional password """
        if password:
            self.send('JOIN %s %s' % (channel, password))
        else:
            self.send('JOIN %s' % channel)

    def part(self, channel):
        """ leave channel """
        self.send('PART %s' % channel)

    def who(self, who):
        """ send who query """
        self.send('WHO %s' % who.strip())

    def names(self, channel):
        """ send names query """
        self.send('NAMES %s' % channel)

    def whois(self, who):
        """ send whois query """
        self.send('WHOIS %s' % who)

    def privmsg(self, printto, what):
        """ send privmsg to irc server """
        self.send('PRIVMSG %s :%s' % (printto, what))

    def send(self, txt):
        """ send text to irc server """
        rlog(1, self.name + '.send', txt)
        try:
            self.sock.send(txt[:510]+'\n')
        except Exception, ex:
            try:
                (errno, errstr) = ex
                if errno != 32:
                    raise
                else:
                    rlog(10, self.name, 'broken pipe error .. ignoring')
            except:
                raise

    def voice(self, channel, who):
        """ give voice """
        self.send('MODE %s +v %s' % (channel, who))
 
    def doop(self, channel, who):
        """ give ops """
        self.send('MODE %s +o %s' % (channel, who))

    def delop(self, channel, who):
        """ de-op user """
        self.send('MODE %s -o %s' % (channel, who))

    def quit(self, reason):
        """ send quit message """
        rlog(10, self.name, 'sending quit')
        try:
            self.sock.send('QUIT :%s\n' % reason)
        except:
            pass

    def notice(self, printto, what):
        """ send notice """
        self.send('NOTICE %s :%s' % (printto, what))
 
    def ctcp(self, printto, what):
        """ send ctcp privmsg """
        self.send("PRIVMSG %s :\001%s\001" % (printto, what))

    def ctcpreply(self, printto, what):
        """ send ctcp notice """
        self.send("NOTICE %s :\001%s\001" % (printto, what))

    def action(self, printto, what):
        """ do action """
        self.send("PRIVMSG %s :\001ACTION %s\001" % (printto, what))

    def handle_ievent(self, ievent):
        """ handle ircevent .. dispatch to method """ 
        try:
            # see if the irc object has a method to handle the ievent
            method = getattr(self,'handle_' + ievent.cmnd.lower())
            # try to call method
            try:
                method(ievent)
            except:
                handle_exception()
        except AttributeError:
            pass
        # see if there are wait callbacks
        self.wait.check(ievent)

    def handle_433(self, ievent):
        """ handle nick already taken """
        nick = ievent.postfix.split()[1]
        randomnick = getrandomnick()
        self.changenick(randomnick)
        rlog(100, self.name, 'ALERT: nick %s already in use .. using \
randomnick %s' % (nick, randomnick))

    def handle_ping(self, ievent):
        """ send pong response """
        self.send('PONG %s :%s' % (self.nick, ievent.txt))

    def handle_001(self, ievent):
        """ set connected event """
        self.connected.set()

    def handle_privmsg(self, ievent):
        """ handle privmsg """
        if ievent.txt and ievent.txt[0] == '\001':
            self.handle_ctcp(ievent)
            return 1
        elif '\001' in ievent.txt:
            return 1

    def handle_notice(self, ievent):
        """ handle notice event """
        if ievent.txt and ievent.txt.find('VERSION') != -1:
            self.say(ievent.nick, config['version'], None, 'notice')
            return 1

    def handle_ctcp(self, ievent):
        """ handle client to client request """
        if ievent.txt.find('VERSION') != -1:
            self.ctcpreply(ievent.nick, 'VERSION %s' % config['version'])
        if ievent.txt.find('PING') != -1:
            try:
                pingtime = ievent.txt.split()[1]
                pingtime2 = ievent.txt.split()[2]
                if pingtime:
                    self.ctcpreply(ievent.nick, 'PING ' + pingtime + ' ' + \
pingtime2)
            except:
                pass

    def handle_error(self, ievent):
        """ show error """
        if ievent.txt.startswith('Closing'):
            rlog(10, self.name, ievent.txt)
        else:
            rlog(10, self.name + '.ERROR', ievent.txt)
        #self.shutdown()

    def ping(self):
        """ ping the irc server """
        rlog(1, self.name, 'sending ping')
        try:
            self.sock.send('PING :%s\n' % self.server)
            return 1
        except Exception, ex:
            rlog(10, self.name, "can't send ping: %s" % str(ex))
            return 0
